<template>
  <div class="min-h-screen bg-gradient-to-br from-blue-50 to-purple-50 py-8">
    <div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
      <div class="text-center mb-8">
        <h1 class="text-3xl font-bold text-gray-900 mb-2">m3u8 转 MP4</h1>
        <p class="text-gray-600">输入 m3u8 播放列表链接，在浏览器端尝试将分片合并并导出 MP4。注意：需要被请求资源支持 CORS；转换在浏览器执行，可能消耗大量内存与 CPU。</p>
      </div>

      <!-- 如果 SharedArrayBuffer 不可用，展示清晰的可操作提示 -->
      <div v-if="!sabSupported" class="mb-6 p-4 rounded border border-yellow-300 bg-yellow-50 text-yellow-900">
        <strong>注意：</strong>
        当前浏览器环境不支持 <code>SharedArrayBuffer</code>，这会导致 ffmpeg.wasm / WebAssembly 多线程功能不可用，转换可能失败。
        <div class="mt-2 text-sm">
          可能原因：未通过 HTTPS 提供页面；或服务器未设置必要的安全头（COOP/COEP）。建议：
          <ul class="list-disc ml-5 mt-1">
            <li>使用最新版 Chrome 或 Edge，并通过 HTTPS 访问本站。</li>
            <li>确保服务器添加响应头：<code>Cross-Origin-Opener-Policy: same-origin</code> 和 <code>Cross-Origin-Embedder-Policy: require-corp</code>。</li>
            <li>如果使用 CDN 加载 ffmpeg core/wasm，请确保这些资源支持 CORP/COEP 或把文件托管到同源（public/）以避免跨域问题。</li>
          </ul>
          <div class="mt-2">
            <button @click="checkHeaders" class="secondary-button mr-2">检查当前页面响应头</button>
            <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Opener_Policy" target="_blank" rel="noreferrer" class="underline mr-2">COOP/COEP 文档</a>
            <a href="https://web.dev/coop-coep/" target="_blank" rel="noreferrer" class="underline">实用指南</a>
          </div>
        </div>
      </div>

      <div class="bg-white rounded-lg shadow p-6">
        <label class="block text-sm font-medium text-gray-700">m3u8 链接</label>
        <div class="mt-2 flex space-x-2">
            <input v-model="url" type="text" placeholder="https://.../playlist.m3u8" class="input flex-1" />
            <button @click="start" :disabled="running" class="primary-button">开始转换</button>
            <button @click="cancel" v-if="running" class="ml-2 secondary-button">取消</button>
        </div>

          <div class="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-3">
            <div>
              <label class="block text-sm font-medium text-gray-700">转换模式</label>
              <select v-model="mode" class="input mt-1 w-full">
                <option value="remux_then_reencode">快速 remux（copy）然后回退重编码（推荐）</option>
                <option value="always_reencode">始终重新编码（更稳，但慢）</option>
                <option value="pipe">使用 pipe 输出（尝试，可能更干净）</option>
              </select>
            </div>

            <div>
              <label class="block text-sm font-medium text-gray-700">并发下载分片</label>
              <input type="number" v-model.number="concurrency" min="1" max="50" class="input mt-1 w-full" />
            </div>

            <div>
              <label class="inline-flex items-center mt-2">
                <input type="checkbox" v-model="autoClean" class="form-checkbox" />
                <span class="ml-2 text-sm text-gray-600">完成后自动清理临时文件</span>
              </label>
            </div>

            <div>
              <label class="inline-flex items-center mt-2">
                <input type="checkbox" v-model="showDetails" class="form-checkbox" />
                <span class="ml-2 text-sm text-gray-600">显示转换详情（ffmpeg 日志）</span>
              </label>
            </div>
          </div>

        <div class="mt-4 text-sm text-gray-700">
          <p class="mb-2">进度：{{ progressText }} <span v-if="progressPercent">（{{ progressPercent }}%）</span></p>
          <div class="w-full bg-gray-200 rounded h-2 overflow-hidden mb-2">
            <div :style="{ width: progressPercent + '%'}" class="h-2 bg-green-400"></div>
          </div>
          <div v-if="(showDetails && (log.length || ffmpegLogs.length))" class="bg-gray-50 p-3 rounded text-xs text-gray-600 max-h-44 overflow-auto">
            <div v-for="(l, i) in log" :key="'l'+i">{{ l }}</div>
            <div v-for="(l, i) in ffmpegLogs" :key="'f'+i">ffmpeg: {{ l }}</div>
          </div>
        </div>

        <div v-if="downloadUrl" class="mt-4">
          <a :href="downloadUrl" :download="downloadName" class="primary-button">下载 {{ downloadName }}</a>
        </div>

        <div class="mt-4 text-xs text-gray-500">
          <p>说明：该工具在浏览器中尝试使用 ffmpeg 完成合并/转码，依赖第三方库与被转换源的 CORS 配置。对于大文件或大量分片可能失败。</p>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'

const url = ref('')
// 是否支持 SharedArrayBuffer（需要 COOP/COEP + HTTPS）
const sabSupported = ref(false)
const isSecure = ref(false)
const ffmpegLogs = ref([])

onMounted(() => {
  isSecure.value = typeof window !== 'undefined' && window.location && window.location.protocol === 'https:'
  sabSupported.value = typeof SharedArrayBuffer !== 'undefined'
  if (!sabSupported.value) {
    // 页面加载时记录一条提示（pushLog 在后面声明，是函数声明会被提升）
    try { pushLog('警告：SharedArrayBuffer 不可用，可能缺少 COOP/COEP 或未使用 HTTPS') } catch (e) { /* ignore */ }
  }
})

async function checkHeaders() {
  try {
    // 使用 HEAD 请求检查当前页面的响应头（仅同源受限）
    const res = await fetch(window.location.href, { method: 'HEAD', credentials: 'same-origin' })
    pushLog('响应头检测结果：')
    pushLog(`Cross-Origin-Opener-Policy: ${res.headers.get('cross-origin-opener-policy') || '（未设置）'}`)
    pushLog(`Cross-Origin-Embedder-Policy: ${res.headers.get('cross-origin-embedder-policy') || '（未设置）'}`)
  } catch (e) {
    pushLog('检查响应头失败：' + (e && e.message ? e.message : String(e)))
  }
}

const running = ref(false)
const log = ref([])
const progressText = ref('未开始')
const progressPercent = ref(0)
const downloadUrl = ref('')
const downloadName = ref('output.mp4')

// UI controls
const mode = ref('remux_then_reencode') // remux_then_reencode | always_reencode | pipe
const autoClean = ref(false)
const concurrency = ref(6)
const showDetails = ref(false)

let abortController = null

function pushLog(msg) {
  log.value.push(`${new Date().toLocaleTimeString()} - ${msg}`)
  // keep tail
  if (log.value.length > 200) log.value.shift()
}

function cancel() {
  if (abortController) {
    abortController.abort()
    pushLog('已请求取消操作')
  }
  running.value = false
  progressText.value = '已取消'
}

function resolveUrl(base, relative) {
  try { return new URL(relative, base).toString() } catch (e) { return relative }
}

async function fetchText(u) {
  const res = await fetch(u, { signal: abortController && abortController.signal })
  if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
  return await res.text()
}

// 解析 m3u8，返回分片绝对 URL 列表
function parseM3U8(text, baseUrl) {
  const lines = text.split(/\r?\n/).map(l => l.trim()).filter(l => l !== '')
  const segments = []
  for (const line of lines) {
    if (line.startsWith('#')) continue
    // 可能是 URI
    segments.push(resolveUrl(baseUrl, line))
  }
  return segments
}

async function start() {
  if (!url.value) return pushLog('请输入 m3u8 链接')
  running.value = true
  log.value = []
  progressText.value = '开始'
  abortController = new AbortController()
  downloadUrl.value = ''

  try {
    pushLog(`获取 m3u8: ${url.value}`)
    const playlistText = await fetchText(url.value)
    pushLog('m3u8 已获取，开始解析')
    // 检测内容是否为标准 m3u8（包含 #EXTM3U 或 #EXTINF 等标签）
    const isM3U8 = /#EXTM3U|#EXTINF/i.test(playlistText)
    if (!isM3U8) {
      pushLog('检测到资源不是 m3u8 播放列表，尝试作为单个媒体文件处理')
      try {
        const resMedia = await fetch(url.value, { signal: abortController && abortController.signal })
        if (!resMedia.ok) throw new Error(`HTTP ${resMedia.status} ${resMedia.statusText}`)
        const contentType = (resMedia.headers.get && resMedia.headers.get('content-type')) || ''
        pushLog(`源 Content-Type: ${contentType || '未知'}`)
        const ab = await resMedia.arrayBuffer()
        // 如果是视频（mp4/fmp4/mpeg-ts 等），直接生成下载链接，避免把二进制当成 m3u8 分片
        if (/video|audio|application\/mp4|application\/octet-stream/i.test(contentType) || url.value.toLowerCase().endsWith('.mp4')) {
          const blob = new Blob([ab], { type: contentType || 'application/octet-stream' })
          if (downloadUrl.value) URL.revokeObjectURL(downloadUrl.value)
          downloadUrl.value = URL.createObjectURL(blob)
          const ext = url.value.toLowerCase().endsWith('.mp4') || /mp4/i.test(contentType) ? '.mp4' : ''
          downloadName.value = `download_${Date.now()}${ext}`
          progressText.value = '完成（单文件）'
          pushLog('资源为单文件媒体，已生成下载链接（无需合并）')
          running.value = false
          return
        }
        // 否则继续当成 m3u8 处理（尽管未检测到标签），以防特殊情况
      } catch (mediaErr) {
        pushLog('作为单文件处理失败：' + (mediaErr && mediaErr.message ? mediaErr.message : String(mediaErr)))
        // 继续后续的 m3u8 解析尝试
      }
    }
    const base = url.value
    const segments = parseM3U8(playlistText, base)
    if (!segments.length) throw new Error('未检测到分片（请确认这是标准 m3u8 列表与 CORS 设置）')
    pushLog(`发现 ${segments.length} 个分片（将按顺序合并）`)

    // 动态加载 ffmpeg 库（尝试从包导入，若失败再尝试 CDN）
    pushLog('加载 ffmpeg 库（这一步可能较慢）')
    progressText.value = '加载 ffmpeg'

    // 使用 CDN 注入 UMD 脚本并从全局对象获取 createFFmpeg/fetchFile，避免在未安装包时触发 Vite 的导入解析错误
    let createFFmpeg, fetchFile
    // 首先尝试直接从 window 上读取（如果被其它脚本或 dev 环境预先注入）
    if (window.createFFmpeg && window.fetchFile) {
      createFFmpeg = window.createFFmpeg
      fetchFile = window.fetchFile
      pushLog('已从全局环境获取 createFFmpeg & fetchFile')
    } else {
      pushLog('本地未检测到 ffmpeg，尝试从 CDN 动态加载（UMD）')
      await new Promise((resolve, reject) => {
  const script = document.createElement('script')
  script.src = 'https://unpkg.com/@ffmpeg/ffmpeg@0.11.6/dist/ffmpeg.min.js'
        script.async = true
        script.onload = resolve
        script.onerror = () => reject(new Error('加载 CDN 脚本失败'))
        document.head.appendChild(script)
      })
      // @ts-ignore
      const FF = window.FFmpeg || window.FFmpegCore || window.ffmpeg
      // 多种 UMD 打包可能把 createFFmpeg 直接挂到 window 或挂在 FFmpeg 下
      if (window.createFFmpeg) {
        createFFmpeg = window.createFFmpeg
        fetchFile = window.fetchFile
      } else if (FF && FF.createFFmpeg) {
        createFFmpeg = FF.createFFmpeg
        fetchFile = FF.fetchFile
      } else if (FF && FF.FFmpeg && FF.FFmpeg.createFFmpeg) {
        createFFmpeg = FF.FFmpeg.createFFmpeg
        fetchFile = FF.FFmpeg.fetchFile
      }
        if (!createFFmpeg) throw new Error('无法从 CDN 获取 createFFmpeg（全局变量缺失）')
        pushLog('已从 CDN 加载 ffmpeg')
    }

    // 检查是否支持SharedArrayBuffer
    if (typeof SharedArrayBuffer === 'undefined') {
      throw new Error('您的浏览器不支持SharedArrayBuffer，这可能是由于缺少必要的安全头（COOP/COEP）。请尝试使用最新版Chrome或Edge浏览器，并确保网站使用HTTPS协议。')
    }
    
    const ffmpeg = createFFmpeg({
      log: true,
      // 指向与 UMD 版本匹配的 core-dist（使用 jsDelivr）
      corePath: 'https://unpkg.com/@ffmpeg/core@0.11.0/dist/ffmpeg-core.js'
    })

    // 捕获 ffmpeg 日志，便于调试失败原因
    if (typeof ffmpeg.setLogger === 'function') {
      ffmpeg.setLogger(({ type, message }) => {
        // 保留一定长度，避免无限增长
        ffmpegLogs.value.push(`${type}: ${message}`)
        if (ffmpegLogs.value.length > 1000) ffmpegLogs.value.shift()
        try { pushLog('ffmpeg: ' + message) } catch (e) { /* noop */ }
      })
    }

    ffmpeg.setProgress(({ ratio }) => {
      const pct = Math.min(100, Math.round(ratio * 100))
      progressPercent.value = pct
      progressText.value = `ffmpeg 进度：${pct}%`
    })
    await ffmpeg.load()
    pushLog('ffmpeg 已加载')

    // 下载分片并写入 FS（并发控制）
    pushLog('开始下载并写入分片到虚拟文件系统（可能耗时）')
    const downloadQueue = segments.map((url, idx) => ({ url, idx }))
    async function worker() {
      while (downloadQueue.length) {
        const item = downloadQueue.shift()
        if (!item) break
        const i = item.idx
        const segUrl = item.url
        if (abortController && abortController.signal.aborted) throw new Error('已取消')
        progressText.value = `下载分片 ${i + 1}/${segments.length}`
        pushLog(`下载 ${segUrl}`)
        const res = await fetch(segUrl, { signal: abortController && abortController.signal })
        if (!res.ok) throw new Error(`下载分片失败：${res.status} ${res.statusText}`)
        try { pushLog(`分片 HTTP Content-Type: ${res.headers.get('content-type') || '（未返回）'}`) } catch (e) { /* ignore */ }
        const ab = await res.arrayBuffer()
        try {
          const head = new Uint8Array(ab.slice(0, 16))
          const hex = Array.from(head).map(b => b.toString(16).padStart(2, '0')).join(' ')
          pushLog(`分片 ${i} 前 16 字节：${hex}`)
          if (head[0] !== 0x47) pushLog('警告：分片首字节不是 0x47，可能不是 MPEG-TS（例如 fMP4 或 HTML）')
        } catch (e) { pushLog('读取分片头部失败：' + (e && e.message ? e.message : String(e))) }
        const name = `seg${String(i).padStart(4, '0')}.ts`
        ffmpeg.FS('writeFile', name, new Uint8Array(ab))
        pushLog(`写入 ${name} (${ab.byteLength} bytes)`)
      }
    }
    const workers = []
    const n = Math.max(1, Math.min(50, concurrency.value || 6))
    for (let w = 0; w < n; w++) workers.push(worker())
    await Promise.all(workers)

    // 创建 concat 列表
  // 生成 concat 列表（不使用 ./ 前缀，某些 wasm/UMD 构建在解析 ./ 时会失败）
  const listContent = segments.map((_, i) => `file 'seg${String(i).padStart(4, '0')}.ts'`).join('\n')
    // 使用 TextEncoder 明确以 UTF-8 字节写入，确保没有 BOM 并以换行结尾
    try {
      const encoder = new TextEncoder()
      ffmpeg.FS('writeFile', 'mylist.txt', encoder.encode(listContent + '\n'))
      pushLog('已创建 mylist.txt（字节写入），用于 concat')
    } catch (e) {
      // 退回到字符串写入（保底）
      ffmpeg.FS('writeFile', 'mylist.txt', listContent)
      pushLog('已创建 mylist.txt（字符串写入，回退）')
    }

    // Debug: 读取并打印 mylist.txt 的前若干行，检查虚拟文件系统中分片是否存在及大小
    try {
      const listBuf = ffmpeg.FS('readFile', 'mylist.txt')
      const listText = new TextDecoder().decode(listBuf)
      pushLog('mylist.txt 内容（前 20 行）：')
      listText.split('\n').slice(0, 20).forEach((ln) => pushLog('  ' + ln))
    } catch (e) {
      pushLog('读取 mylist.txt 失败：' + (e && e.message ? e.message : String(e)))
    }

    try {
      const files = ffmpeg.FS('readdir', '.')
      pushLog('虚拟文件系统初步列表：' + files.join(', '))
    } catch (e) {
      pushLog('读取虚拟文件系统失败：' + (e && e.message ? e.message : String(e)))
    }

    // 对每个分片做存在/大小检查，便于诊断 concat 打不开文件的原因
    for (let i = 0; i < segments.length; i++) {
      const name = `seg${String(i).padStart(4, '0')}.ts`
      try {
        const st = ffmpeg.FS('stat', name)
        pushLog(`${name} 存在，size=${st.size}`)
      } catch (e) {
        pushLog(`${name} 不存在或无法 stat：` + (e && e.message ? e.message : String(e)))
      }
    }

    // 运行 ffmpeg 合并并封装为 mp4（copy 方法尽量快，不重新编码）
    progressText.value = '合并分片为 MP4'
    pushLog('开始运行 ffmpeg 合并分片（此步骤可能占用较多时间）')

  try {
    // 在尝试前，打印第一个分片的前几个字节（魔数检测），以确认是否为 MPEG-TS
    try {
      const head = ffmpeg.FS('readFile', 'seg0000.ts').slice(0, 16)
      const hex = Array.from(head).map(b => b.toString(16).padStart(2, '0')).join(' ')
      pushLog(`seg0000.ts 前 16 字节：${hex}`)
    } catch (e) {
      pushLog('无法读取 seg0000.ts 头部：' + (e && e.message ? e.message : String(e)))
    }

    // 尝试多个 mylist.txt 格式（不同的引号/路径风格），以应对 wasm/UMD 下 concat demuxer 对路径解析的差异
  const listVariants = [
      // 带单引号（当前默认）
      segments.map((_, i) => `file 'seg${String(i).padStart(4, '0')}.ts'`).join('\n'),
      // 无引号
      segments.map((_, i) => `file seg${String(i).padStart(4, '0')}.ts`).join('\n'),
      // 带 ./ 前缀
      segments.map((_, i) => `file './seg${String(i).padStart(4, '0')}.ts'`).join('\n'),
      // 绝对路径（根目录）
      segments.map((_, i) => `file '/seg${String(i).padStart(4, '0')}.ts'`).join('\n')
    ]

    let ffmpegSucceeded = false
    let lastErr = null
    for (let vi = 0; vi < listVariants.length; vi++) {
      const variant = listVariants[vi]
      try {
        // 写入 mylist.txt（优先使用 CRLF，若失败退回到 LF，再退回到字符串写入）
        try {
          const encoder = new TextEncoder()
          try {
            ffmpeg.FS('writeFile', 'mylist.txt', encoder.encode(variant + '\r\n'))
          } catch (e1) {
            try {
              ffmpeg.FS('writeFile', 'mylist.txt', encoder.encode(variant + '\n'))
            } catch (e2) {
              ffmpeg.FS('writeFile', 'mylist.txt', variant)
            }
          }
        } catch (e) {
          // 如果 TextEncoder 不可用，退回到字符串写入
          ffmpeg.FS('writeFile', 'mylist.txt', variant)
        }
        pushLog(`已写入 mylist.txt（variant ${vi + 1}/${listVariants.length}）`)
        // 读取并打印前几行，便于排查
        try {
          const buf = ffmpeg.FS('readFile', 'mylist.txt')
          const txt = new TextDecoder().decode(buf)
          pushLog('mylist.txt (尝试格式预览):')
          txt.split('\n').slice(0, 10).forEach(l => pushLog('  ' + l))
        } catch (e) { /* ignore */ }

  await ffmpeg.run('-protocol_whitelist', 'file,pipe,concat', '-f', 'concat', '-safe', '0', '-i', 'mylist.txt', '-c', 'copy', '-y', '-nostdin', 'output.mp4')
        // 有些 ffmpeg.wasm 构建可能在 stderr 输出错误信息但仍然返回（或行为不一致），
        // 因此在把本次尝试视为成功前，显式检查 output.mp4 是否由 FFmpeg 生成。
        try {
          ffmpeg.FS('stat', 'output.mp4')
          pushLog(`ffmpeg 在 variant ${vi + 1} 成功运行并生成 output.mp4`)
          ffmpegSucceeded = true
          break
        } catch (statErr) {
          // output.mp4 不存在，视为本次尝试失败，记录并继续下个 variant
          pushLog(`variant ${vi + 1} 未生成 output.mp4，视为失败，继续尝试下一个格式`) 
          lastErr = new Error('ffmpeg 未生成 output.mp4')
          try { ffmpeg.FS('unlink', 'output.mp4') } catch (e) { /* ignore */ }
          // 继续下一轮
        }
      } catch (e) {
        lastErr = e
        pushLog(`variant ${vi + 1} 运行失败：` + (e && e.message ? e.message : String(e)))
        // 打印 ffmpeg 尾部日志，帮助判断失败原因
        const tail = ffmpegLogs.value.slice(-40)
        tail.forEach(l => pushLog(l))
        // 清理可能的 output.mp4 残留
        try { ffmpeg.FS('unlink', 'output.mp4') } catch (e2) { /* ignore */ }
      }
    }

    try {
      if (!ffmpegSucceeded) {
        // 若所有 variant 都失败且错误是无法打开输入，则执行已有的 combined.ts 回退路径
        const errMsg = (lastErr && lastErr.message) ? lastErr.message : String(lastErr)
        if (/Impossible to open|Invalid data found when processing input/.test(errMsg)) {
          pushLog('所有 mylist.txt 格式尝试均失败，准备使用回退的 combined.ts 合并方法')
        }
      } else {
        pushLog('ffmpeg 成功生成 output.mp4（通过 mylist.txt）')
      }
    } catch (e) {
      pushLog('在判断 ffmpeg 成功状态时发生错误：' + (e && e.message ? e.message : String(e)))
    }

  // 如果未成功，再进入原先的 catch 路径（combined.ts 回退）
    if (!ffmpegSucceeded) {
      // 在放弃前，尝试使用 ffmpeg 的 concat 协议作为另一种回退（仅在分片数量不太多时尝试）
      try {
        if (segments.length > 0 && segments.length <= 200) {
          const concatInput = 'concat:' + segments.map((_, i) => `seg${String(i).padStart(4, '0')}.ts`).join('|')
          pushLog('尝试使用 concat 协议回退（concat:...）')
          try {
            // 包含 concat 协议到白名单，避免被 wasm ffmpeg 拒绝
            await ffmpeg.run('-protocol_whitelist', 'file,pipe,concat', '-i', concatInput, '-c', 'copy', '-y', '-nostdin', 'output.mp4')
            // 检查输出
            ffmpeg.FS('stat', 'output.mp4')
            pushLog('concat 协议回退成功，已生成 output.mp4')
            ffmpegSucceeded = true
          } catch (cpErr) {
            pushLog('concat 协议回退失败：' + (cpErr && cpErr.message ? cpErr.message : String(cpErr)))
            try { ffmpeg.FS('unlink', 'output.mp4') } catch (e) { /* ignore */ }
          }
        }
      } catch (e) {
        pushLog('尝试 concat 协议回退时发生错误：' + (e && e.message ? e.message : String(e)))
      }
      // 抛出一个标识性错误，让下面的 catch 捕获并执行回退逻辑
      throw lastErr || new Error('ffmpeg concat 多格式尝试全部失败')
    }

    // 若成功，继续后续流程（读取输出）
    pushLog('ffmpeg 运行完成，准备读取输出')

    } catch (e) {
      // 如果 ffmpeg.run 失败，输出最近的日志并抛出更清晰的错误
      pushLog('ffmpeg 运行时发生错误：' + (e && e.message ? e.message : String(e)))
      pushLog('------ ffmpeg 日志（尾部若干行） ------')
      const tail = ffmpegLogs.value.slice(-80)
      tail.forEach(l => pushLog(l))
      // 列出虚拟文件系统，帮助诊断输出文件是否生成
      try {
        const files = ffmpeg.FS('readdir', '.')
        pushLog('虚拟文件系统内容：' + files.join(', '))
      } catch (e2) {
        pushLog('读取虚拟文件系统失败：' + (e2 && e2.message ? e2.message : String(e2)))
      }
      // 如果 concat demuxer 无法打开分片（例如日志包含 Impossible to open），尝试回退：
      const errMsg = (e && e.message) ? e.message : String(e)
      // 无论错误类型，尝试回退：在内存中合并所有分片为 combined.ts 并再次封装（可能消耗大量内存）
      pushLog('开始回退: 将所有分片二进制在内存中合并为 combined.ts 并再次封装（可能消耗大量内存）')
      try {
        // 逐个读取分片并拼接到一个大 ArrayBuffer
        let total = 0
        const parts = []
        for (let i = 0; i < segments.length; i++) {
          const name = `seg${String(i).padStart(4, '0')}.ts`
          try {
            const chunk = ffmpeg.FS('readFile', name)
            parts.push(chunk)
            total += chunk.length
            pushLog(`读取 ${name} (${chunk.length} bytes) 用于合并`)
          } catch (readErr) {
            pushLog(`读取 ${name} 失败：` + (readErr && readErr.message ? readErr.message : String(readErr)))
            throw readErr
          }
        }
        pushLog(`开始在内存中合并 ${parts.length} 个分片，总大小 ${total} bytes`)
        const combined = new Uint8Array(total)
        let offset = 0
        for (const p of parts) {
          combined.set(p, offset)
          offset += p.length
        }
        ffmpeg.FS('writeFile', 'combined.ts', combined)
        pushLog('已生成 combined.ts，开始用 ffmpeg 将其封装为 output.mp4（使用 protocol_whitelist）')
            // 根据模式决定是否先 remux 或直接重新编码，或尝试 pipe
            if (mode.value === 'always_reencode') {
              pushLog('模式：始终重新编码，直接执行重新编码流程')
              await ffmpeg.run(
                '-protocol_whitelist', 'file,pipe,concat',
                '-f', 'mpegts',
                '-i', 'combined.ts',
                '-c:v', 'libx264',
                '-preset', 'veryfast',
                '-crf', '23',
                '-c:a', 'aac',
                '-b:a', '128k',
                '-movflags', '+faststart',
                '-y', '-nostdin',
                'output.mp4'
              )
              pushLog('combined.ts 重新编码完成，生成 output.mp4')
            } else if (mode.value === 'pipe') {
              pushLog('模式：pipe，尝试将输出写到 stdout (pipe:1)')
              try {
                // 尝试把输出写入 pipe:1（注意：在某些 ffmpeg.wasm 实现中对 pipe 的支持有限）
                await ffmpeg.run('-protocol_whitelist', 'file,pipe,concat', '-f', 'mpegts', '-i', 'combined.ts', '-c', 'copy', '-f', 'mp4', 'pipe:1')
                // 在许多 wasm 构建中，pipe:1 的输出不会成为虚拟 FS 文件，回退到 FS 方法
                pushLog('pipe 输出尝试已完成（若未生成文件将回退到文件方式）')
              } catch (pipeErr) {
                pushLog('pipe 输出失败，回退到 remux(copy) 或重新编码：' + (pipeErr && pipeErr.message ? pipeErr.message : String(pipeErr)))
                // 继续下面的 remux/reencode 尝试
              }
            }

            // 如果不是强制重新编码模式，先尝试快速 remux(copy)
            if (mode.value !== 'always_reencode') {
              try {
                await ffmpeg.run('-protocol_whitelist', 'file,pipe,concat', '-f', 'mpegts', '-i', 'combined.ts', '-c', 'copy', '-y', '-nostdin', 'output.mp4')
                pushLog('combined.ts remux (copy) 成功，生成 output.mp4')
              } catch (remuxCopyErr) {
                pushLog('combined.ts remux (copy) 失败，准备重新编码：' + (remuxCopyErr && remuxCopyErr.message ? remuxCopyErr.message : String(remuxCopyErr)))
                // 重新编码作为回退
                await ffmpeg.run(
                  '-protocol_whitelist', 'file,pipe,concat',
                  '-f', 'mpegts',
                  '-i', 'combined.ts',
                  '-c:v', 'libx264',
                  '-preset', 'veryfast',
                  '-crf', '23',
                  '-c:a', 'aac',
                  '-b:a', '128k',
                  '-movflags', '+faststart',
                  '-y', '-nostdin',
                  'output.mp4'
                )
                pushLog('combined.ts 重新编码完成，生成 output.mp4')
              }
            }
        } catch (fallbackErr) {
          pushLog('回退合并方法失败：' + (fallbackErr && fallbackErr.message ? fallbackErr.message : String(fallbackErr)))
          pushLog('------ ffmpeg 日志（尾部若干行） ------')
          const tail2 = ffmpegLogs.value.slice(-200)
          tail2.forEach(l => pushLog(l))

          // combined.ts 回退失败，尝试更强力但更耗资源的 remux-to-TS 回退：
          // 对每个分片执行 remux -> segNNNN.remux.ts（-c copy -f mpegts），然后 concat 合并 remuxed 文件
          try {
            pushLog('尝试 remux-to-TS 回退（对每个分片 remux 到 MPEG-TS，然后 concat）')
            // 保护：限制分片数量与总大小，避免浏览器 OOM
            const MAX_SEGMENTS = 500
            if (segments.length > MAX_SEGMENTS) throw new Error(`分片过多（${segments.length}），跳过 remux 回退以防止浏览器资源耗尽`)

            // 逐个 remux
            for (let i = 0; i < segments.length; i++) {
              const name = `seg${String(i).padStart(4, '0')}.ts`
              const remuxName = `seg${String(i).padStart(4, '0')}.remux.ts`
              pushLog(`开始 remux ${name} -> ${remuxName} (${i + 1}/${segments.length})`)
              try {
                // remux：-i name -c copy -f mpegts remuxName
                await ffmpeg.run('-protocol_whitelist', 'file,pipe,concat', '-i', name, '-c', 'copy', '-f', 'mpegts', '-y', '-nostdin', remuxName)
                pushLog(`remux 完成：${remuxName}`)
              } catch (remuxErr) {
                pushLog(`remux 失败 ${name}：` + (remuxErr && remuxErr.message ? remuxErr.message : String(remuxErr)))
                throw remuxErr
              }
            }

            // 生成 remux 列表并合并
            const remuxList = segments.map((_, i) => `file 'seg${String(i).padStart(4, '0')}.remux.ts'`).join('\r\n') + '\r\n'
            try {
              // 写入 mylist_remux.txt
              const encoder = new TextEncoder()
              ffmpeg.FS('writeFile', 'mylist_remux.txt', encoder.encode(remuxList))
              pushLog('已写入 mylist_remux.txt，开始用 concat 合并 remuxed 分片')
            } catch (e) {
              // 回退到字符串写入
              ffmpeg.FS('writeFile', 'mylist_remux.txt', remuxList)
              pushLog('已写入 mylist_remux.txt（字符串回退）')
            }

            try {
              await ffmpeg.run('-protocol_whitelist', 'file,pipe,concat', '-f', 'concat', '-safe', '0', '-i', 'mylist_remux.txt', '-c', 'copy', '-y', '-nostdin', 'output.mp4')
              // 检查输出文件
              ffmpeg.FS('stat', 'output.mp4')
              pushLog('remux-to-TS 回退成功，已生成 output.mp4')
            } catch (remuxConcatErr) {
              pushLog('remux-to-TS 合并失败：' + (remuxConcatErr && remuxConcatErr.message ? remuxConcatErr.message : String(remuxConcatErr)))
              pushLog('------ ffmpeg 日志（尾部若干行） ------')
              const tail3 = ffmpegLogs.value.slice(-200)
              tail3.forEach(l => pushLog(l))
              throw remuxConcatErr
            }

            // 如果执行到这里，remux 回退成功，跳出并继续后续读取输出流程
          } catch (remuxAllErr) {
            pushLog('remux-to-TS 回退最终失败：' + (remuxAllErr && remuxAllErr.message ? remuxAllErr.message : String(remuxAllErr)))
            throw fallbackErr
          }
        }
    }

    // 列出虚拟文件系统并选择最可能的输出文件：优先选择 *.mp4 中最大的，其次选择最大文件
    let outputName = 'output.mp4'
    try {
      const files = ffmpeg.FS('readdir', '.')
      pushLog('转换完成后虚拟文件系统内容：' + files.join(', '))

      // 优先在 .mp4 文件中选择最大的那个
      let candidate = null
      let candidateSize = 0
      for (const f of files) {
        try {
          const st = ffmpeg.FS('stat', f)
          if (!st || !st.size) continue
          const lower = f.toLowerCase()
          if (lower.endsWith('.mp4')) {
            if (st.size > candidateSize) {
              candidate = f
              candidateSize = st.size
            }
          }
        } catch (se) { /* ignore */ }
      }

      // 如果没有 mp4，选择最大的文件（排除非常小的临时文件）
      if (!candidate) {
        for (const f of files) {
          try {
            const st = ffmpeg.FS('stat', f)
            if (!st || !st.size) continue
            if (st.size > candidateSize && st.size > 1024) {
              candidate = f
              candidateSize = st.size
            }
          } catch (se) { /* ignore */ }
        }
      }

      if (candidate) {
        outputName = candidate
        pushLog(`选择输出文件：${outputName} (${candidateSize} bytes)`)
      } else {
        pushLog('未能在虚拟文件系统中找到合适的输出文件')
        pushLog('------ ffmpeg 日志（尾部若干行） ------')
        const tail = ffmpegLogs.value.slice(-200)
        tail.forEach(l => pushLog(l))
        throw new Error('输出文件不存在，ffmpeg 可能在转码/合并阶段失败。查看上方日志以获取更多信息。')
      }
    } catch (e) {
      pushLog('读取虚拟文件系统或选择输出文件失败：' + (e && e.message ? e.message : String(e)))
      throw e
    }

    const data = ffmpeg.FS('readFile', outputName)
    const blob = new Blob([data.buffer], { type: 'video/mp4' })
    if (downloadUrl.value) URL.revokeObjectURL(downloadUrl.value)
    downloadUrl.value = URL.createObjectURL(blob)
    downloadName.value = `converted_${Date.now()}.mp4`
    progressText.value = '完成'
    pushLog('转换成功，可点击下载')

    // 可选：自动清理临时文件
    if (autoClean.value) {
      pushLog('开始清理虚拟文件系统中的临时文件 (autoClean=true)')
      try {
        const files = ffmpeg.FS('readdir', '.')
        for (const f of files) {
          try {
            if (/^seg\d{4}\.ts$/.test(f) || f === 'combined.ts' || f.startsWith('mylist')) {
              try { ffmpeg.FS('unlink', f) } catch (e) { /* ignore */ }
              pushLog(`已删除 ${f}`)
            }
          } catch (se) { /* ignore */ }
        }
      } catch (ce) {
        pushLog('清理虚拟文件系统失败：' + (ce && ce.message ? ce.message : String(ce)))
      }
    }
  } catch (err) {
    const msg = err && err.message ? err.message : String(err)
    pushLog('错误：' + msg)
    progressText.value = '失败：' + msg
  } finally {
    running.value = false
  }
}

useHead({
  title: 'm3u8 转 MP4 - 视频工具 - 八八在线工具'
})
</script>

<!-- 使用项目的 Tailwind 全局样式，无需在此处重复定义 -->
